{
 "nbformat_minor": 1, 
 "nbformat": 4, 
 "cells": [
  {
   "source": [
    "# What's this PyTorch business?\n", 
    "\n", 
    "You've written a lot of code in this assignment to provide a whole host of neural network functionality. Dropout, Batch Norm, and 2D convolutions are some of the workhorses of deep learning in computer vision. You've also worked hard to make your code efficient and vectorized.\n", 
    "\n", 
    "For the last part of this assignment, though, we're going to leave behind your beautiful codebase and instead migrate to one of two popular deep learning frameworks: in this instance, PyTorch (or TensorFlow, if you choose to use that notebook)."
   ], 
   "cell_type": "markdown", 
   "metadata": {
    "tags": [
     "pdf-title"
    ]
   }
  }, 
  {
   "source": [
    "### What is PyTorch?\n", 
    "\n", 
    "PyTorch is a system for executing dynamic computational graphs over Tensor objects that behave similarly as numpy ndarray. It comes with a powerful automatic differentiation engine that removes the need for manual back-propagation. \n", 
    "\n", 
    "### Why?\n", 
    "\n", 
    "* Our code will now run on GPUs! Much faster training. When using a framework like PyTorch or TensorFlow you can harness the power of the GPU for your own custom neural network architectures without having to write CUDA code directly (which is beyond the scope of this class).\n", 
    "* We want you to be ready to use one of these frameworks for your project so you can experiment more efficiently than if you were writing every feature you want to use by hand. \n", 
    "* We want you to stand on the shoulders of giants! TensorFlow and PyTorch are both excellent frameworks that will make your lives a lot easier, and now that you understand their guts, you are free to use them :) \n", 
    "* We want you to be exposed to the sort of deep learning code you might run into in academia or industry.\n", 
    "\n", 
    "### PyTorch versions\n", 
    "This notebook assumes that you are using **PyTorch version 1.0**. In some of the previous versions (e.g. before 0.4), Tensors had to be wrapped in Variable objects to be used in autograd; however Variables have now been deprecated. In addition 1.0 also separates a Tensor's datatype from its device, and uses numpy-style factories for constructing Tensors rather than directly invoking Tensor constructors."
   ], 
   "cell_type": "markdown", 
   "metadata": {
    "tags": [
     "pdf-ignore"
    ]
   }
  }, 
  {
   "source": [
    "## How will I learn PyTorch?\n", 
    "\n", 
    "Justin Johnson has made an excellent [tutorial](https://github.com/jcjohnson/pytorch-examples) for PyTorch. \n", 
    "\n", 
    "You can also find the detailed [API doc](http://pytorch.org/docs/stable/index.html) here. If you have other questions that are not addressed by the API docs, the [PyTorch forum](https://discuss.pytorch.org/) is a much better place to ask than StackOverflow.\n", 
    "\n", 
    "\n", 
    "# Table of Contents\n", 
    "\n", 
    "This assignment has 5 parts. You will learn PyTorch on **three different levels of abstraction**, which will help you understand it better and prepare you for the final project. \n", 
    "\n", 
    "1. Part I, Preparation: we will use CIFAR-10 dataset.\n", 
    "2. Part II, Barebones PyTorch: **Abstraction level 1**, we will work directly with the lowest-level PyTorch Tensors. \n", 
    "3. Part III, PyTorch Module API: **Abstraction level 2**, we will use `nn.Module` to define arbitrary neural network architecture. \n", 
    "4. Part IV, PyTorch Sequential API: **Abstraction level 3**, we will use `nn.Sequential` to define a linear feed-forward network very conveniently. \n", 
    "5. Part V, CIFAR-10 open-ended challenge: please implement your own network to get as high accuracy as possible on CIFAR-10. You can experiment with any layer, optimizer, hyperparameters or other advanced features. \n", 
    "\n", 
    "Here is a table of comparison:\n", 
    "\n", 
    "| API           | Flexibility | Convenience |\n", 
    "|---------------|-------------|-------------|\n", 
    "| Barebone      | High        | Low         |\n", 
    "| `nn.Module`     | High        | Medium      |\n", 
    "| `nn.Sequential` | Low         | High        |"
   ], 
   "cell_type": "markdown", 
   "metadata": {
    "tags": [
     "pdf-ignore"
    ]
   }
  }, 
  {
   "source": [
    "# Part I. Preparation\n", 
    "\n", 
    "First, we load the CIFAR-10 dataset. This might take a couple minutes the first time you do it, but the files should stay cached after that.\n", 
    "\n", 
    "In previous parts of the assignment we had to write our own code to download the CIFAR-10 dataset, preprocess it, and iterate through it in minibatches; PyTorch provides convenient tools to automate this process for us."
   ], 
   "cell_type": "markdown", 
   "metadata": {}
  }, 
  {
   "execution_count": null, 
   "cell_type": "code", 
   "source": [
    "import torch\n", 
    "import torch.nn as nn\n", 
    "import torch.optim as optim\n", 
    "from torch.utils.data import DataLoader\n", 
    "from torch.utils.data import sampler\n", 
    "\n", 
    "import torchvision.datasets as dset\n", 
    "import torchvision.transforms as T\n", 
    "\n", 
    "import numpy as np"
   ], 
   "outputs": [], 
   "metadata": {
    "tags": [
     "pdf-ignore"
    ]
   }
  }, 
  {
   "execution_count": null, 
   "cell_type": "code", 
   "source": [
    "NUM_TRAIN = 49000\n", 
    "\n", 
    "# The torchvision.transforms package provides tools for preprocessing data\n", 
    "# and for performing data augmentation; here we set up a transform to\n", 
    "# preprocess the data by subtracting the mean RGB value and dividing by the\n", 
    "# standard deviation of each RGB value; we've hardcoded the mean and std.\n", 
    "transform = T.Compose([\n", 
    "                T.ToTensor(),\n", 
    "                T.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))\n", 
    "            ])\n", 
    "\n", 
    "# We set up a Dataset object for each split (train / val / test); Datasets load\n", 
    "# training examples one at a time, so we wrap each Dataset in a DataLoader which\n", 
    "# iterates through the Dataset and forms minibatches. We divide the CIFAR-10\n", 
    "# training set into train and val sets by passing a Sampler object to the\n", 
    "# DataLoader telling how it should sample from the underlying Dataset.\n", 
    "cifar10_train = dset.CIFAR10('./cs231n/datasets', train=True, download=True,\n", 
    "                             transform=transform)\n", 
    "loader_train = DataLoader(cifar10_train, batch_size=64, \n", 
    "                          sampler=sampler.SubsetRandomSampler(range(NUM_TRAIN)))\n", 
    "\n", 
    "cifar10_val = dset.CIFAR10('./cs231n/datasets', train=True, download=True,\n", 
    "                           transform=transform)\n", 
    "loader_val = DataLoader(cifar10_val, batch_size=64, \n", 
    "                        sampler=sampler.SubsetRandomSampler(range(NUM_TRAIN, 50000)))\n", 
    "\n", 
    "cifar10_test = dset.CIFAR10('./cs231n/datasets', train=False, download=True, \n", 
    "                            transform=transform)\n", 
    "loader_test = DataLoader(cifar10_test, batch_size=64)"
   ], 
   "outputs": [], 
   "metadata": {
    "tags": [
     "pdf-ignore"
    ]
   }
  }, 
  {
   "source": [
    "You have an option to **use GPU by setting the flag to True below**. It is not necessary to use GPU for this assignment. Note that if your computer does not have CUDA enabled, `torch.cuda.is_available()` will return False and this notebook will fallback to CPU mode.\n", 
    "\n", 
    "The global variables `dtype` and `device` will control the data types throughout this assignment. "
   ], 
   "cell_type": "markdown", 
   "metadata": {
    "tags": [
     "pdf-ignore"
    ]
   }
  }, 
  {
   "execution_count": null, 
   "cell_type": "code", 
   "source": [
    "USE_GPU = True\n", 
    "\n", 
    "dtype = torch.float32 # we will be using float throughout this tutorial\n", 
    "\n", 
    "if USE_GPU and torch.cuda.is_available():\n", 
    "    device = torch.device('cuda')\n", 
    "else:\n", 
    "    device = torch.device('cpu')\n", 
    "\n", 
    "# Constant to control how frequently we print train loss\n", 
    "print_every = 100\n", 
    "\n", 
    "print('using device:', device)"
   ], 
   "outputs": [], 
   "metadata": {
    "tags": [
     "pdf-ignore-input"
    ]
   }
  }, 
  {
   "source": [
    "# Part II. Barebones PyTorch\n", 
    "\n", 
    "PyTorch ships with high-level APIs to help us define model architectures conveniently, which we will cover in Part II of this tutorial. In this section, we will start with the barebone PyTorch elements to understand the autograd engine better. After this exercise, you will come to appreciate the high-level model API more.\n", 
    "\n", 
    "We will start with a simple fully-connected ReLU network with two hidden layers and no biases for CIFAR classification. \n", 
    "This implementation computes the forward pass using operations on PyTorch Tensors, and uses PyTorch autograd to compute gradients. It is important that you understand every line, because you will write a harder version after the example.\n", 
    "\n", 
    "When we create a PyTorch Tensor with `requires_grad=True`, then operations involving that Tensor will not just compute values; they will also build up a computational graph in the background, allowing us to easily backpropagate through the graph to compute gradients of some Tensors with respect to a downstream loss. Concretely if x is a Tensor with `x.requires_grad == True` then after backpropagation `x.grad` will be another Tensor holding the gradient of x with respect to the scalar loss at the end."
   ], 
   "cell_type": "markdown", 
   "metadata": {}
  }, 
  {
   "source": [
    "### PyTorch Tensors: Flatten Function\n", 
    "A PyTorch Tensor is conceptionally similar to a numpy array: it is an n-dimensional grid of numbers, and like numpy PyTorch provides many functions to efficiently operate on Tensors. As a simple example, we provide a `flatten` function below which reshapes image data for use in a fully-connected neural network.\n", 
    "\n", 
    "Recall that image data is typically stored in a Tensor of shape N x C x H x W, where:\n", 
    "\n", 
    "* N is the number of datapoints\n", 
    "* C is the number of channels\n", 
    "* H is the height of the intermediate feature map in pixels\n", 
    "* W is the height of the intermediate feature map in pixels\n", 
    "\n", 
    "This is the right way to represent the data when we are doing something like a 2D convolution, that needs spatial understanding of where the intermediate features are relative to each other. When we use fully connected affine layers to process the image, however, we want each datapoint to be represented by a single vector -- it's no longer useful to segregate the different channels, rows, and columns of the data. So, we use a \"flatten\" operation to collapse the `C x H x W` values per representation into a single long vector. The flatten function below first reads in the N, C, H, and W values from a given batch of data, and then returns a \"view\" of that data. \"View\" is analogous to numpy's \"reshape\" method: it reshapes x's dimensions to be N x ??, where ?? is allowed to be anything (in this case, it will be C x H x W, but we don't need to specify that explicitly). "
   ], 
   "cell_type": "markdown", 
   "metadata": {
    "tags": [
     "pdf-ignore"
    ]
   }
  }, 
  {
   "execution_count": null, 
   "cell_type": "code", 
   "source": [
    "def flatten(x):\n", 
    "    N = x.shape[0] # read in N, C, H, W\n", 
    "    return x.view(N, -1)  # \"flatten\" the C * H * W values into a single vector per image\n", 
    "\n", 
    "def test_flatten():\n", 
    "    x = torch.arange(12).view(2, 1, 3, 2)\n", 
    "    print('Before flattening: ', x)\n", 
    "    print('After flattening: ', flatten(x))\n", 
    "\n", 
    "test_flatten()"
   ], 
   "outputs": [], 
   "metadata": {
    "tags": [
     "pdf-ignore-input"
    ]
   }
  }, 
  {
   "source": [
    "### Barebones PyTorch: Two-Layer Network\n", 
    "\n", 
    "Here we define a function `two_layer_fc` which performs the forward pass of a two-layer fully-connected ReLU network on a batch of image data. After defining the forward pass we check that it doesn't crash and that it produces outputs of the right shape by running zeros through the network.\n", 
    "\n", 
    "You don't have to write any code here, but it's important that you read and understand the implementation."
   ], 
   "cell_type": "markdown", 
   "metadata": {
    "tags": [
     "pdf-ignore"
    ]
   }
  }, 
  {
   "execution_count": null, 
   "cell_type": "code", 
   "source": [
    "import torch.nn.functional as F  # useful stateless functions\n", 
    "\n", 
    "def two_layer_fc(x, params):\n", 
    "    \"\"\"\n", 
    "    A fully-connected neural networks; the architecture is:\n", 
    "    NN is fully connected -> ReLU -> fully connected layer.\n", 
    "    Note that this function only defines the forward pass; \n", 
    "    PyTorch will take care of the backward pass for us.\n", 
    "    \n", 
    "    The input to the network will be a minibatch of data, of shape\n", 
    "    (N, d1, ..., dM) where d1 * ... * dM = D. The hidden layer will have H units,\n", 
    "    and the output layer will produce scores for C classes.\n", 
    "    \n", 
    "    Inputs:\n", 
    "    - x: A PyTorch Tensor of shape (N, d1, ..., dM) giving a minibatch of\n", 
    "      input data.\n", 
    "    - params: A list [w1, w2] of PyTorch Tensors giving weights for the network;\n", 
    "      w1 has shape (D, H) and w2 has shape (H, C).\n", 
    "    \n", 
    "    Returns:\n", 
    "    - scores: A PyTorch Tensor of shape (N, C) giving classification scores for\n", 
    "      the input data x.\n", 
    "    \"\"\"\n", 
    "    # first we flatten the image\n", 
    "    x = flatten(x)  # shape: [batch_size, C x H x W]\n", 
    "    \n", 
    "    w1, w2 = params\n", 
    "    \n", 
    "    # Forward pass: compute predicted y using operations on Tensors. Since w1 and\n", 
    "    # w2 have requires_grad=True, operations involving these Tensors will cause\n", 
    "    # PyTorch to build a computational graph, allowing automatic computation of\n", 
    "    # gradients. Since we are no longer implementing the backward pass by hand we\n", 
    "    # don't need to keep references to intermediate values.\n", 
    "    # you can also use `.clamp(min=0)`, equivalent to F.relu()\n", 
    "    x = F.relu(x.mm(w1))\n", 
    "    x = x.mm(w2)\n", 
    "    return x\n", 
    "    \n", 
    "\n", 
    "def two_layer_fc_test():\n", 
    "    hidden_layer_size = 42\n", 
    "    x = torch.zeros((64, 50), dtype=dtype)  # minibatch size 64, feature dimension 50\n", 
    "    w1 = torch.zeros((50, hidden_layer_size), dtype=dtype)\n", 
    "    w2 = torch.zeros((hidden_layer_size, 10), dtype=dtype)\n", 
    "    scores = two_layer_fc(x, [w1, w2])\n", 
    "    print(scores.size())  # you should see [64, 10]\n", 
    "\n", 
    "two_layer_fc_test()"
   ], 
   "outputs": [], 
   "metadata": {
    "tags": [
     "pdf-ignore-input"
    ]
   }
  }, 
  {
   "source": [
    "### Barebones PyTorch: Three-Layer ConvNet\n", 
    "\n", 
    "Here you will complete the implementation of the function `three_layer_convnet`, which will perform the forward pass of a three-layer convolutional network. Like above, we can immediately test our implementation by passing zeros through the network. The network should have the following architecture:\n", 
    "\n", 
    "1. A convolutional layer (with bias) with `channel_1` filters, each with shape `KW1 x KH1`, and zero-padding of two\n", 
    "2. ReLU nonlinearity\n", 
    "3. A convolutional layer (with bias) with `channel_2` filters, each with shape `KW2 x KH2`, and zero-padding of one\n", 
    "4. ReLU nonlinearity\n", 
    "5. Fully-connected layer with bias, producing scores for C classes.\n", 
    "\n", 
    "Note that we have **no softmax activation** here after our fully-connected layer: this is because PyTorch's cross entropy loss performs a softmax activation for you, and by bundling that step in makes computation more efficient.\n", 
    "\n", 
    "**HINT**: For convolutions: http://pytorch.org/docs/stable/nn.html#torch.nn.functional.conv2d; pay attention to the shapes of convolutional filters!"
   ], 
   "cell_type": "markdown", 
   "metadata": {}
  }, 
  {
   "execution_count": null, 
   "cell_type": "code", 
   "source": [
    "def three_layer_convnet(x, params):\n", 
    "    \"\"\"\n", 
    "    Performs the forward pass of a three-layer convolutional network with the\n", 
    "    architecture defined above.\n", 
    "\n", 
    "    Inputs:\n", 
    "    - x: A PyTorch Tensor of shape (N, 3, H, W) giving a minibatch of images\n", 
    "    - params: A list of PyTorch Tensors giving the weights and biases for the\n", 
    "      network; should contain the following:\n", 
    "      - conv_w1: PyTorch Tensor of shape (channel_1, 3, KH1, KW1) giving weights\n", 
    "        for the first convolutional layer\n", 
    "      - conv_b1: PyTorch Tensor of shape (channel_1,) giving biases for the first\n", 
    "        convolutional layer\n", 
    "      - conv_w2: PyTorch Tensor of shape (channel_2, channel_1, KH2, KW2) giving\n", 
    "        weights for the second convolutional layer\n", 
    "      - conv_b2: PyTorch Tensor of shape (channel_2,) giving biases for the second\n", 
    "        convolutional layer\n", 
    "      - fc_w: PyTorch Tensor giving weights for the fully-connected layer. Can you\n", 
    "        figure out what the shape should be?\n", 
    "      - fc_b: PyTorch Tensor giving biases for the fully-connected layer. Can you\n", 
    "        figure out what the shape should be?\n", 
    "    \n", 
    "    Returns:\n", 
    "    - scores: PyTorch Tensor of shape (N, C) giving classification scores for x\n", 
    "    \"\"\"\n", 
    "    conv_w1, conv_b1, conv_w2, conv_b2, fc_w, fc_b = params\n", 
    "    scores = None\n", 
    "    ################################################################################\n", 
    "    # TODO: Implement the forward pass for the three-layer ConvNet.                #\n", 
    "    ################################################################################\n", 
    "    # *****START OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****\n\n", 
    "    pass\n", 
    "\n    # *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****\n", 
    "    ################################################################################\n", 
    "    #                                 END OF YOUR CODE                             #\n", 
    "    ################################################################################\n", 
    "    return scores"
   ], 
   "outputs": [], 
   "metadata": {}
  }, 
  {
   "source": [
    "After defining the forward pass of the ConvNet above, run the following cell to test your implementation.\n", 
    "\n", 
    "When you run this function, scores should have shape (64, 10)."
   ], 
   "cell_type": "markdown", 
   "metadata": {}
  }, 
  {
   "execution_count": null, 
   "cell_type": "code", 
   "source": [
    "def three_layer_convnet_test():\n", 
    "    x = torch.zeros((64, 3, 32, 32), dtype=dtype)  # minibatch size 64, image size [3, 32, 32]\n", 
    "\n", 
    "    conv_w1 = torch.zeros((6, 3, 5, 5), dtype=dtype)  # [out_channel, in_channel, kernel_H, kernel_W]\n", 
    "    conv_b1 = torch.zeros((6,))  # out_channel\n", 
    "    conv_w2 = torch.zeros((9, 6, 3, 3), dtype=dtype)  # [out_channel, in_channel, kernel_H, kernel_W]\n", 
    "    conv_b2 = torch.zeros((9,))  # out_channel\n", 
    "\n", 
    "    # you must calculate the shape of the tensor after two conv layers, before the fully-connected layer\n", 
    "    fc_w = torch.zeros((9 * 32 * 32, 10))\n", 
    "    fc_b = torch.zeros(10)\n", 
    "\n", 
    "    scores = three_layer_convnet(x, [conv_w1, conv_b1, conv_w2, conv_b2, fc_w, fc_b])\n", 
    "    print(scores.size())  # you should see [64, 10]\n", 
    "three_layer_convnet_test()"
   ], 
   "outputs": [], 
   "metadata": {
    "tags": [
     "pdf-ignore-input"
    ]
   }
  }, 
  {
   "source": [
    "### Barebones PyTorch: Initialization\n", 
    "Let's write a couple utility methods to initialize the weight matrices for our models.\n", 
    "\n", 
    "- `random_weight(shape)` initializes a weight tensor with the Kaiming normalization method.\n", 
    "- `zero_weight(shape)` initializes a weight tensor with all zeros. Useful for instantiating bias parameters.\n", 
    "\n", 
    "The `random_weight` function uses the Kaiming normal initialization method, described in:\n", 
    "\n", 
    "He et al, *Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification*, ICCV 2015, https://arxiv.org/abs/1502.01852"
   ], 
   "cell_type": "markdown", 
   "metadata": {}
  }, 
  {
   "execution_count": null, 
   "cell_type": "code", 
   "source": [
    "def random_weight(shape):\n", 
    "    \"\"\"\n", 
    "    Create random Tensors for weights; setting requires_grad=True means that we\n", 
    "    want to compute gradients for these Tensors during the backward pass.\n", 
    "    We use Kaiming normalization: sqrt(2 / fan_in)\n", 
    "    \"\"\"\n", 
    "    if len(shape) == 2:  # FC weight\n", 
    "        fan_in = shape[0]\n", 
    "    else:\n", 
    "        fan_in = np.prod(shape[1:]) # conv weight [out_channel, in_channel, kH, kW]\n", 
    "    # randn is standard normal distribution generator. \n", 
    "    w = torch.randn(shape, device=device, dtype=dtype) * np.sqrt(2. / fan_in)\n", 
    "    w.requires_grad = True\n", 
    "    return w\n", 
    "\n", 
    "def zero_weight(shape):\n", 
    "    return torch.zeros(shape, device=device, dtype=dtype, requires_grad=True)\n", 
    "\n", 
    "# create a weight of shape [3 x 5]\n", 
    "# you should see the type `torch.cuda.FloatTensor` if you use GPU. \n", 
    "# Otherwise it should be `torch.FloatTensor`\n", 
    "random_weight((3, 5))"
   ], 
   "outputs": [], 
   "metadata": {
    "tags": [
     "pdf-ignore-input"
    ]
   }
  }, 
  {
   "source": [
    "### Barebones PyTorch: Check Accuracy\n", 
    "When training the model we will use the following function to check the accuracy of our model on the training or validation sets.\n", 
    "\n", 
    "When checking accuracy we don't need to compute any gradients; as a result we don't need PyTorch to build a computational graph for us when we compute scores. To prevent a graph from being built we scope our computation under a `torch.no_grad()` context manager."
   ], 
   "cell_type": "markdown", 
   "metadata": {}
  }, 
  {
   "execution_count": null, 
   "cell_type": "code", 
   "source": [
    "def check_accuracy_part2(loader, model_fn, params):\n", 
    "    \"\"\"\n", 
    "    Check the accuracy of a classification model.\n", 
    "    \n", 
    "    Inputs:\n", 
    "    - loader: A DataLoader for the data split we want to check\n", 
    "    - model_fn: A function that performs the forward pass of the model,\n", 
    "      with the signature scores = model_fn(x, params)\n", 
    "    - params: List of PyTorch Tensors giving parameters of the model\n", 
    "    \n", 
    "    Returns: Nothing, but prints the accuracy of the model\n", 
    "    \"\"\"\n", 
    "    split = 'val' if loader.dataset.train else 'test'\n", 
    "    print('Checking accuracy on the %s set' % split)\n", 
    "    num_correct, num_samples = 0, 0\n", 
    "    with torch.no_grad():\n", 
    "        for x, y in loader:\n", 
    "            x = x.to(device=device, dtype=dtype)  # move to device, e.g. GPU\n", 
    "            y = y.to(device=device, dtype=torch.int64)\n", 
    "            scores = model_fn(x, params)\n", 
    "            _, preds = scores.max(1)\n", 
    "            num_correct += (preds == y).sum()\n", 
    "            num_samples += preds.size(0)\n", 
    "        acc = float(num_correct) / num_samples\n", 
    "        print('Got %d / %d correct (%.2f%%)' % (num_correct, num_samples, 100 * acc))"
   ], 
   "outputs": [], 
   "metadata": {
    "tags": [
     "pdf-ignore-input"
    ]
   }
  }, 
  {
   "source": [
    "### BareBones PyTorch: Training Loop\n", 
    "We can now set up a basic training loop to train our network. We will train the model using stochastic gradient descent without momentum. We will use `torch.functional.cross_entropy` to compute the loss; you can [read about it here](http://pytorch.org/docs/stable/nn.html#cross-entropy).\n", 
    "\n", 
    "The training loop takes as input the neural network function, a list of initialized parameters (`[w1, w2]` in our example), and learning rate."
   ], 
   "cell_type": "markdown", 
   "metadata": {}
  }, 
  {
   "execution_count": null, 
   "cell_type": "code", 
   "source": [
    "def train_part2(model_fn, params, learning_rate):\n", 
    "    \"\"\"\n", 
    "    Train a model on CIFAR-10.\n", 
    "    \n", 
    "    Inputs:\n", 
    "    - model_fn: A Python function that performs the forward pass of the model.\n", 
    "      It should have the signature scores = model_fn(x, params) where x is a\n", 
    "      PyTorch Tensor of image data, params is a list of PyTorch Tensors giving\n", 
    "      model weights, and scores is a PyTorch Tensor of shape (N, C) giving\n", 
    "      scores for the elements in x.\n", 
    "    - params: List of PyTorch Tensors giving weights for the model\n", 
    "    - learning_rate: Python scalar giving the learning rate to use for SGD\n", 
    "    \n", 
    "    Returns: Nothing\n", 
    "    \"\"\"\n", 
    "    for t, (x, y) in enumerate(loader_train):\n", 
    "        # Move the data to the proper device (GPU or CPU)\n", 
    "        x = x.to(device=device, dtype=dtype)\n", 
    "        y = y.to(device=device, dtype=torch.long)\n", 
    "\n", 
    "        # Forward pass: compute scores and loss\n", 
    "        scores = model_fn(x, params)\n", 
    "        loss = F.cross_entropy(scores, y)\n", 
    "\n", 
    "        # Backward pass: PyTorch figures out which Tensors in the computational\n", 
    "        # graph has requires_grad=True and uses backpropagation to compute the\n", 
    "        # gradient of the loss with respect to these Tensors, and stores the\n", 
    "        # gradients in the .grad attribute of each Tensor.\n", 
    "        loss.backward()\n", 
    "\n", 
    "        # Update parameters. We don't want to backpropagate through the\n", 
    "        # parameter updates, so we scope the updates under a torch.no_grad()\n", 
    "        # context manager to prevent a computational graph from being built.\n", 
    "        with torch.no_grad():\n", 
    "            for w in params:\n", 
    "                w -= learning_rate * w.grad\n", 
    "\n", 
    "                # Manually zero the gradients after running the backward pass\n", 
    "                w.grad.zero_()\n", 
    "\n", 
    "        if t % print_every == 0:\n", 
    "            print('Iteration %d, loss = %.4f' % (t, loss.item()))\n", 
    "            check_accuracy_part2(loader_val, model_fn, params)\n", 
    "            print()"
   ], 
   "outputs": [], 
   "metadata": {
    "tags": [
     "pdf-ignore-input"
    ]
   }
  }, 
  {
   "source": [
    "### BareBones PyTorch: Train a Two-Layer Network\n", 
    "Now we are ready to run the training loop. We need to explicitly allocate tensors for the fully connected weights, `w1` and `w2`. \n", 
    "\n", 
    "Each minibatch of CIFAR has 64 examples, so the tensor shape is `[64, 3, 32, 32]`. \n", 
    "\n", 
    "After flattening, `x` shape should be `[64, 3 * 32 * 32]`. This will be the size of the first dimension of `w1`. \n", 
    "The second dimension of `w1` is the hidden layer size, which will also be the first dimension of `w2`. \n", 
    "\n", 
    "Finally, the output of the network is a 10-dimensional vector that represents the probability distribution over 10 classes. \n", 
    "\n", 
    "You don't need to tune any hyperparameters but you should see accuracies above 40% after training for one epoch."
   ], 
   "cell_type": "markdown", 
   "metadata": {}
  }, 
  {
   "execution_count": null, 
   "cell_type": "code", 
   "source": [
    "hidden_layer_size = 4000\n", 
    "learning_rate = 1e-2\n", 
    "\n", 
    "w1 = random_weight((3 * 32 * 32, hidden_layer_size))\n", 
    "w2 = random_weight((hidden_layer_size, 10))\n", 
    "\n", 
    "train_part2(two_layer_fc, [w1, w2], learning_rate)"
   ], 
   "outputs": [], 
   "metadata": {}
  }, 
  {
   "source": [
    "### BareBones PyTorch: Training a ConvNet\n", 
    "\n", 
    "In the below you should use the functions defined above to train a three-layer convolutional network on CIFAR. The network should have the following architecture:\n", 
    "\n", 
    "1. Convolutional layer (with bias) with 32 5x5 filters, with zero-padding of 2\n", 
    "2. ReLU\n", 
    "3. Convolutional layer (with bias) with 16 3x3 filters, with zero-padding of 1\n", 
    "4. ReLU\n", 
    "5. Fully-connected layer (with bias) to compute scores for 10 classes\n", 
    "\n", 
    "You should initialize your weight matrices using the `random_weight` function defined above, and you should initialize your bias vectors using the `zero_weight` function above.\n", 
    "\n", 
    "You don't need to tune any hyperparameters, but if everything works correctly you should achieve an accuracy above 42% after one epoch."
   ], 
   "cell_type": "markdown", 
   "metadata": {}
  }, 
  {
   "execution_count": null, 
   "cell_type": "code", 
   "source": [
    "learning_rate = 3e-3\n", 
    "\n", 
    "channel_1 = 32\n", 
    "channel_2 = 16\n", 
    "\n", 
    "conv_w1 = None\n", 
    "conv_b1 = None\n", 
    "conv_w2 = None\n", 
    "conv_b2 = None\n", 
    "fc_w = None\n", 
    "fc_b = None\n", 
    "\n", 
    "################################################################################\n", 
    "# TODO: Initialize the parameters of a three-layer ConvNet.                    #\n", 
    "################################################################################\n", 
    "# *****START OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****\n\n", 
    "pass\n", 
    "\n# *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****\n", 
    "################################################################################\n", 
    "#                                 END OF YOUR CODE                             #\n", 
    "################################################################################\n", 
    "\n", 
    "params = [conv_w1, conv_b1, conv_w2, conv_b2, fc_w, fc_b]\n", 
    "train_part2(three_layer_convnet, params, learning_rate)"
   ], 
   "outputs": [], 
   "metadata": {}
  }, 
  {
   "source": [
    "# Part III. PyTorch Module API\n", 
    "\n", 
    "Barebone PyTorch requires that we track all the parameter tensors by hand. This is fine for small networks with a few tensors, but it would be extremely inconvenient and error-prone to track tens or hundreds of tensors in larger networks.\n", 
    "\n", 
    "PyTorch provides the `nn.Module` API for you to define arbitrary network architectures, while tracking every learnable parameters for you. In Part II, we implemented SGD ourselves. PyTorch also provides the `torch.optim` package that implements all the common optimizers, such as RMSProp, Adagrad, and Adam. It even supports approximate second-order methods like L-BFGS! You can refer to the [doc](http://pytorch.org/docs/master/optim.html) for the exact specifications of each optimizer.\n", 
    "\n", 
    "To use the Module API, follow the steps below:\n", 
    "\n", 
    "1. Subclass `nn.Module`. Give your network class an intuitive name like `TwoLayerFC`. \n", 
    "\n", 
    "2. In the constructor `__init__()`, define all the layers you need as class attributes. Layer objects like `nn.Linear` and `nn.Conv2d` are themselves `nn.Module` subclasses and contain learnable parameters, so that you don't have to instantiate the raw tensors yourself. `nn.Module` will track these internal parameters for you. Refer to the [doc](http://pytorch.org/docs/master/nn.html) to learn more about the dozens of builtin layers. **Warning**: don't forget to call the `super().__init__()` first!\n", 
    "\n", 
    "3. In the `forward()` method, define the *connectivity* of your network. You should use the attributes defined in `__init__` as function calls that take tensor as input and output the \"transformed\" tensor. Do *not* create any new layers with learnable parameters in `forward()`! All of them must be declared upfront in `__init__`. \n", 
    "\n", 
    "After you define your Module subclass, you can instantiate it as an object and call it just like the NN forward function in part II.\n", 
    "\n", 
    "### Module API: Two-Layer Network\n", 
    "Here is a concrete example of a 2-layer fully connected network:"
   ], 
   "cell_type": "markdown", 
   "metadata": {}
  }, 
  {
   "execution_count": null, 
   "cell_type": "code", 
   "source": [
    "class TwoLayerFC(nn.Module):\n", 
    "    def __init__(self, input_size, hidden_size, num_classes):\n", 
    "        super().__init__()\n", 
    "        # assign layer objects to class attributes\n", 
    "        self.fc1 = nn.Linear(input_size, hidden_size)\n", 
    "        # nn.init package contains convenient initialization methods\n", 
    "        # http://pytorch.org/docs/master/nn.html#torch-nn-init \n", 
    "        nn.init.kaiming_normal_(self.fc1.weight)\n", 
    "        self.fc2 = nn.Linear(hidden_size, num_classes)\n", 
    "        nn.init.kaiming_normal_(self.fc2.weight)\n", 
    "    \n", 
    "    def forward(self, x):\n", 
    "        # forward always defines connectivity\n", 
    "        x = flatten(x)\n", 
    "        scores = self.fc2(F.relu(self.fc1(x)))\n", 
    "        return scores\n", 
    "\n", 
    "def test_TwoLayerFC():\n", 
    "    input_size = 50\n", 
    "    x = torch.zeros((64, input_size), dtype=dtype)  # minibatch size 64, feature dimension 50\n", 
    "    model = TwoLayerFC(input_size, 42, 10)\n", 
    "    scores = model(x)\n", 
    "    print(scores.size())  # you should see [64, 10]\n", 
    "test_TwoLayerFC()"
   ], 
   "outputs": [], 
   "metadata": {}
  }, 
  {
   "source": [
    "### Module API: Three-Layer ConvNet\n", 
    "It's your turn to implement a 3-layer ConvNet followed by a fully connected layer. The network architecture should be the same as in Part II:\n", 
    "\n", 
    "1. Convolutional layer with `channel_1` 5x5 filters with zero-padding of 2\n", 
    "2. ReLU\n", 
    "3. Convolutional layer with `channel_2` 3x3 filters with zero-padding of 1\n", 
    "4. ReLU\n", 
    "5. Fully-connected layer to `num_classes` classes\n", 
    "\n", 
    "You should initialize the weight matrices of the model using the Kaiming normal initialization method.\n", 
    "\n", 
    "**HINT**: http://pytorch.org/docs/stable/nn.html#conv2d\n", 
    "\n", 
    "After you implement the three-layer ConvNet, the `test_ThreeLayerConvNet` function will run your implementation; it should print `(64, 10)` for the shape of the output scores."
   ], 
   "cell_type": "markdown", 
   "metadata": {}
  }, 
  {
   "execution_count": null, 
   "cell_type": "code", 
   "source": [
    "class ThreeLayerConvNet(nn.Module):\n", 
    "    def __init__(self, in_channel, channel_1, channel_2, num_classes):\n", 
    "        super().__init__()\n", 
    "        ########################################################################\n", 
    "        # TODO: Set up the layers you need for a three-layer ConvNet with the  #\n", 
    "        # architecture defined above.                                          #\n", 
    "        ########################################################################\n", 
    "        # *****START OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****\n\n", 
    "        pass\n", 
    "\n        # *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****\n", 
    "        ########################################################################\n", 
    "        #                          END OF YOUR CODE                            #       \n", 
    "        ########################################################################\n", 
    "\n", 
    "    def forward(self, x):\n", 
    "        scores = None\n", 
    "        ########################################################################\n", 
    "        # TODO: Implement the forward function for a 3-layer ConvNet. you      #\n", 
    "        # should use the layers you defined in __init__ and specify the        #\n", 
    "        # connectivity of those layers in forward()                            #\n", 
    "        ########################################################################\n", 
    "        # *****START OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****\n\n", 
    "        pass\n", 
    "\n        # *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****\n", 
    "        ########################################################################\n", 
    "        #                             END OF YOUR CODE                         #\n", 
    "        ########################################################################\n", 
    "        return scores\n", 
    "\n", 
    "\n", 
    "def test_ThreeLayerConvNet():\n", 
    "    x = torch.zeros((64, 3, 32, 32), dtype=dtype)  # minibatch size 64, image size [3, 32, 32]\n", 
    "    model = ThreeLayerConvNet(in_channel=3, channel_1=12, channel_2=8, num_classes=10)\n", 
    "    scores = model(x)\n", 
    "    print(scores.size())  # you should see [64, 10]\n", 
    "test_ThreeLayerConvNet()"
   ], 
   "outputs": [], 
   "metadata": {}
  }, 
  {
   "source": [
    "### Module API: Check Accuracy\n", 
    "Given the validation or test set, we can check the classification accuracy of a neural network. \n", 
    "\n", 
    "This version is slightly different from the one in part II. You don't manually pass in the parameters anymore."
   ], 
   "cell_type": "markdown", 
   "metadata": {}
  }, 
  {
   "execution_count": null, 
   "cell_type": "code", 
   "source": [
    "def check_accuracy_part34(loader, model):\n", 
    "    if loader.dataset.train:\n", 
    "        print('Checking accuracy on validation set')\n", 
    "    else:\n", 
    "        print('Checking accuracy on test set')   \n", 
    "    num_correct = 0\n", 
    "    num_samples = 0\n", 
    "    model.eval()  # set model to evaluation mode\n", 
    "    with torch.no_grad():\n", 
    "        for x, y in loader:\n", 
    "            x = x.to(device=device, dtype=dtype)  # move to device, e.g. GPU\n", 
    "            y = y.to(device=device, dtype=torch.long)\n", 
    "            scores = model(x)\n", 
    "            _, preds = scores.max(1)\n", 
    "            num_correct += (preds == y).sum()\n", 
    "            num_samples += preds.size(0)\n", 
    "        acc = float(num_correct) / num_samples\n", 
    "        print('Got %d / %d correct (%.2f)' % (num_correct, num_samples, 100 * acc))"
   ], 
   "outputs": [], 
   "metadata": {}
  }, 
  {
   "source": [
    "### Module API: Training Loop\n", 
    "We also use a slightly different training loop. Rather than updating the values of the weights ourselves, we use an Optimizer object from the `torch.optim` package, which abstract the notion of an optimization algorithm and provides implementations of most of the algorithms commonly used to optimize neural networks."
   ], 
   "cell_type": "markdown", 
   "metadata": {}
  }, 
  {
   "execution_count": null, 
   "cell_type": "code", 
   "source": [
    "def train_part34(model, optimizer, epochs=1):\n", 
    "    \"\"\"\n", 
    "    Train a model on CIFAR-10 using the PyTorch Module API.\n", 
    "    \n", 
    "    Inputs:\n", 
    "    - model: A PyTorch Module giving the model to train.\n", 
    "    - optimizer: An Optimizer object we will use to train the model\n", 
    "    - epochs: (Optional) A Python integer giving the number of epochs to train for\n", 
    "    \n", 
    "    Returns: Nothing, but prints model accuracies during training.\n", 
    "    \"\"\"\n", 
    "    model = model.to(device=device)  # move the model parameters to CPU/GPU\n", 
    "    for e in range(epochs):\n", 
    "        for t, (x, y) in enumerate(loader_train):\n", 
    "            model.train()  # put model to training mode\n", 
    "            x = x.to(device=device, dtype=dtype)  # move to device, e.g. GPU\n", 
    "            y = y.to(device=device, dtype=torch.long)\n", 
    "\n", 
    "            scores = model(x)\n", 
    "            loss = F.cross_entropy(scores, y)\n", 
    "\n", 
    "            # Zero out all of the gradients for the variables which the optimizer\n", 
    "            # will update.\n", 
    "            optimizer.zero_grad()\n", 
    "\n", 
    "            # This is the backwards pass: compute the gradient of the loss with\n", 
    "            # respect to each  parameter of the model.\n", 
    "            loss.backward()\n", 
    "\n", 
    "            # Actually update the parameters of the model using the gradients\n", 
    "            # computed by the backwards pass.\n", 
    "            optimizer.step()\n", 
    "\n", 
    "            if t % print_every == 0:\n", 
    "                print('Iteration %d, loss = %.4f' % (t, loss.item()))\n", 
    "                check_accuracy_part34(loader_val, model)\n", 
    "                print()"
   ], 
   "outputs": [], 
   "metadata": {}
  }, 
  {
   "source": [
    "### Module API: Train a Two-Layer Network\n", 
    "Now we are ready to run the training loop. In contrast to part II, we don't explicitly allocate parameter tensors anymore.\n", 
    "\n", 
    "Simply pass the input size, hidden layer size, and number of classes (i.e. output size) to the constructor of `TwoLayerFC`. \n", 
    "\n", 
    "You also need to define an optimizer that tracks all the learnable parameters inside `TwoLayerFC`.\n", 
    "\n", 
    "You don't need to tune any hyperparameters, but you should see model accuracies above 40% after training for one epoch."
   ], 
   "cell_type": "markdown", 
   "metadata": {}
  }, 
  {
   "execution_count": null, 
   "cell_type": "code", 
   "source": [
    "hidden_layer_size = 4000\n", 
    "learning_rate = 1e-2\n", 
    "model = TwoLayerFC(3 * 32 * 32, hidden_layer_size, 10)\n", 
    "optimizer = optim.SGD(model.parameters(), lr=learning_rate)\n", 
    "\n", 
    "train_part34(model, optimizer)"
   ], 
   "outputs": [], 
   "metadata": {}
  }, 
  {
   "source": [
    "### Module API: Train a Three-Layer ConvNet\n", 
    "You should now use the Module API to train a three-layer ConvNet on CIFAR. This should look very similar to training the two-layer network! You don't need to tune any hyperparameters, but you should achieve above above 45% after training for one epoch.\n", 
    "\n", 
    "You should train the model using stochastic gradient descent without momentum."
   ], 
   "cell_type": "markdown", 
   "metadata": {}
  }, 
  {
   "execution_count": null, 
   "cell_type": "code", 
   "source": [
    "learning_rate = 3e-3\n", 
    "channel_1 = 32\n", 
    "channel_2 = 16\n", 
    "\n", 
    "model = None\n", 
    "optimizer = None\n", 
    "################################################################################\n", 
    "# TODO: Instantiate your ThreeLayerConvNet model and a corresponding optimizer #\n", 
    "################################################################################\n", 
    "# *****START OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****\n\n", 
    "pass\n", 
    "\n# *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****\n", 
    "################################################################################\n", 
    "#                                 END OF YOUR CODE                             \n", 
    "################################################################################\n", 
    "\n", 
    "train_part34(model, optimizer)"
   ], 
   "outputs": [], 
   "metadata": {}
  }, 
  {
   "source": [
    "# Part IV. PyTorch Sequential API\n", 
    "\n", 
    "Part III introduced the PyTorch Module API, which allows you to define arbitrary learnable layers and their connectivity. \n", 
    "\n", 
    "For simple models like a stack of feed forward layers, you still need to go through 3 steps: subclass `nn.Module`, assign layers to class attributes in `__init__`, and call each layer one by one in `forward()`. Is there a more convenient way? \n", 
    "\n", 
    "Fortunately, PyTorch provides a container Module called `nn.Sequential`, which merges the above steps into one. It is not as flexible as `nn.Module`, because you cannot specify more complex topology than a feed-forward stack, but it's good enough for many use cases.\n", 
    "\n", 
    "### Sequential API: Two-Layer Network\n", 
    "Let's see how to rewrite our two-layer fully connected network example with `nn.Sequential`, and train it using the training loop defined above.\n", 
    "\n", 
    "Again, you don't need to tune any hyperparameters here, but you shoud achieve above 40% accuracy after one epoch of training."
   ], 
   "cell_type": "markdown", 
   "metadata": {}
  }, 
  {
   "execution_count": null, 
   "cell_type": "code", 
   "source": [
    "# We need to wrap `flatten` function in a module in order to stack it\n", 
    "# in nn.Sequential\n", 
    "class Flatten(nn.Module):\n", 
    "    def forward(self, x):\n", 
    "        return flatten(x)\n", 
    "\n", 
    "hidden_layer_size = 4000\n", 
    "learning_rate = 1e-2\n", 
    "\n", 
    "model = nn.Sequential(\n", 
    "    Flatten(),\n", 
    "    nn.Linear(3 * 32 * 32, hidden_layer_size),\n", 
    "    nn.ReLU(),\n", 
    "    nn.Linear(hidden_layer_size, 10),\n", 
    ")\n", 
    "\n", 
    "# you can use Nesterov momentum in optim.SGD\n", 
    "optimizer = optim.SGD(model.parameters(), lr=learning_rate,\n", 
    "                     momentum=0.9, nesterov=True)\n", 
    "\n", 
    "train_part34(model, optimizer)"
   ], 
   "outputs": [], 
   "metadata": {}
  }, 
  {
   "source": [
    "### Sequential API: Three-Layer ConvNet\n", 
    "Here you should use `nn.Sequential` to define and train a three-layer ConvNet with the same architecture we used in Part III:\n", 
    "\n", 
    "1. Convolutional layer (with bias) with 32 5x5 filters, with zero-padding of 2\n", 
    "2. ReLU\n", 
    "3. Convolutional layer (with bias) with 16 3x3 filters, with zero-padding of 1\n", 
    "4. ReLU\n", 
    "5. Fully-connected layer (with bias) to compute scores for 10 classes\n", 
    "\n", 
    "You should initialize your weight matrices using the `random_weight` function defined above, and you should initialize your bias vectors using the `zero_weight` function above.\n", 
    "\n", 
    "You should optimize your model using stochastic gradient descent with Nesterov momentum 0.9.\n", 
    "\n", 
    "Again, you don't need to tune any hyperparameters but you should see accuracy above 55% after one epoch of training."
   ], 
   "cell_type": "markdown", 
   "metadata": {}
  }, 
  {
   "execution_count": null, 
   "cell_type": "code", 
   "source": [
    "channel_1 = 32\n", 
    "channel_2 = 16\n", 
    "learning_rate = 1e-2\n", 
    "\n", 
    "model = None\n", 
    "optimizer = None\n", 
    "\n", 
    "################################################################################\n", 
    "# TODO: Rewrite the 2-layer ConvNet with bias from Part III with the           #\n", 
    "# Sequential API.                                                              #\n", 
    "################################################################################\n", 
    "# *****START OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****\n\n", 
    "pass\n", 
    "\n# *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****\n", 
    "################################################################################\n", 
    "#                                 END OF YOUR CODE                             \n", 
    "################################################################################\n", 
    "\n", 
    "train_part34(model, optimizer)"
   ], 
   "outputs": [], 
   "metadata": {}
  }, 
  {
   "source": [
    "# Part V. CIFAR-10 open-ended challenge\n", 
    "\n", 
    "In this section, you can experiment with whatever ConvNet architecture you'd like on CIFAR-10. \n", 
    "\n", 
    "Now it's your job to experiment with architectures, hyperparameters, loss functions, and optimizers to train a model that achieves **at least 70%** accuracy on the CIFAR-10 **validation** set within 10 epochs. You can use the check_accuracy and train functions from above. You can use either `nn.Module` or `nn.Sequential` API. \n", 
    "\n", 
    "Describe what you did at the end of this notebook.\n", 
    "\n", 
    "Here are the official API documentation for each component. One note: what we call in the class \"spatial batch norm\" is called \"BatchNorm2D\" in PyTorch.\n", 
    "\n", 
    "* Layers in torch.nn package: http://pytorch.org/docs/stable/nn.html\n", 
    "* Activations: http://pytorch.org/docs/stable/nn.html#non-linear-activations\n", 
    "* Loss functions: http://pytorch.org/docs/stable/nn.html#loss-functions\n", 
    "* Optimizers: http://pytorch.org/docs/stable/optim.html\n", 
    "\n", 
    "\n", 
    "### Things you might try:\n", 
    "- **Filter size**: Above we used 5x5; would smaller filters be more efficient?\n", 
    "- **Number of filters**: Above we used 32 filters. Do more or fewer do better?\n", 
    "- **Pooling vs Strided Convolution**: Do you use max pooling or just stride convolutions?\n", 
    "- **Batch normalization**: Try adding spatial batch normalization after convolution layers and vanilla batch normalization after affine layers. Do your networks train faster?\n", 
    "- **Network architecture**: The network above has two layers of trainable parameters. Can you do better with a deep network? Good architectures to try include:\n", 
    "    - [conv-relu-pool]xN -> [affine]xM -> [softmax or SVM]\n", 
    "    - [conv-relu-conv-relu-pool]xN -> [affine]xM -> [softmax or SVM]\n", 
    "    - [batchnorm-relu-conv]xN -> [affine]xM -> [softmax or SVM]\n", 
    "- **Global Average Pooling**: Instead of flattening and then having multiple affine layers, perform convolutions until your image gets small (7x7 or so) and then perform an average pooling operation to get to a 1x1 image picture (1, 1 , Filter#), which is then reshaped into a (Filter#) vector. This is used in [Google's Inception Network](https://arxiv.org/abs/1512.00567) (See Table 1 for their architecture).\n", 
    "- **Regularization**: Add l2 weight regularization, or perhaps use Dropout.\n", 
    "\n", 
    "### Tips for training\n", 
    "For each network architecture that you try, you should tune the learning rate and other hyperparameters. When doing this there are a couple important things to keep in mind:\n", 
    "\n", 
    "- If the parameters are working well, you should see improvement within a few hundred iterations\n", 
    "- Remember the coarse-to-fine approach for hyperparameter tuning: start by testing a large range of hyperparameters for just a few training iterations to find the combinations of parameters that are working at all.\n", 
    "- Once you have found some sets of parameters that seem to work, search more finely around these parameters. You may need to train for more epochs.\n", 
    "- You should use the validation set for hyperparameter search, and save your test set for evaluating your architecture on the best parameters as selected by the validation set.\n", 
    "\n", 
    "### Going above and beyond\n", 
    "If you are feeling adventurous there are many other features you can implement to try and improve your performance. You are **not required** to implement any of these, but don't miss the fun if you have time!\n", 
    "\n", 
    "- Alternative optimizers: you can try Adam, Adagrad, RMSprop, etc.\n", 
    "- Alternative activation functions such as leaky ReLU, parametric ReLU, ELU, or MaxOut.\n", 
    "- Model ensembles\n", 
    "- Data augmentation\n", 
    "- New Architectures\n", 
    "  - [ResNets](https://arxiv.org/abs/1512.03385) where the input from the previous layer is added to the output.\n", 
    "  - [DenseNets](https://arxiv.org/abs/1608.06993) where inputs into previous layers are concatenated together.\n", 
    "  - [This blog has an in-depth overview](https://chatbotslife.com/resnets-highwaynets-and-densenets-oh-my-9bb15918ee32)\n", 
    "\n", 
    "### Have fun and happy training! "
   ], 
   "cell_type": "markdown", 
   "metadata": {}
  }, 
  {
   "execution_count": null, 
   "cell_type": "code", 
   "source": [
    "################################################################################\n", 
    "# TODO:                                                                        #         \n", 
    "# Experiment with any architectures, optimizers, and hyperparameters.          #\n", 
    "# Achieve AT LEAST 70% accuracy on the *validation set* within 10 epochs.      #\n", 
    "#                                                                              #\n", 
    "# Note that you can use the check_accuracy function to evaluate on either      #\n", 
    "# the test set or the validation set, by passing either loader_test or         #\n", 
    "# loader_val as the second argument to check_accuracy. You should not touch    #\n", 
    "# the test set until you have finished your architecture and  hyperparameter   #\n", 
    "# tuning, and only run the test set once at the end to report a final value.   #\n", 
    "################################################################################\n", 
    "model = None\n", 
    "optimizer = None\n", 
    "\n", 
    "# *****START OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****\n\n", 
    "pass\n", 
    "\n# *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****\n", 
    "################################################################################\n", 
    "#                                 END OF YOUR CODE                             \n", 
    "################################################################################\n", 
    "\n", 
    "# You should get at least 70% accuracy\n", 
    "train_part34(model, optimizer, epochs=10)"
   ], 
   "outputs": [], 
   "metadata": {}
  }, 
  {
   "source": [
    "## Describe what you did \n", 
    "\n", 
    "In the cell below you should write an explanation of what you did, any additional features that you implemented, and/or any graphs that you made in the process of training and evaluating your network."
   ], 
   "cell_type": "markdown", 
   "metadata": {
    "tags": [
     "pdf-inline"
    ]
   }
  }, 
  {
   "source": [
    "TODO: Describe what you did"
   ], 
   "cell_type": "markdown", 
   "metadata": {
    "tags": [
     "pdf-inline"
    ]
   }
  }, 
  {
   "source": [
    "## Test set -- run this only once\n", 
    "\n", 
    "Now that we've gotten a result we're happy with, we test our final model on the test set (which you should store in best_model). Think about how this compares to your validation set accuracy."
   ], 
   "cell_type": "markdown", 
   "metadata": {}
  }, 
  {
   "execution_count": null, 
   "cell_type": "code", 
   "source": [
    "best_model = model\n", 
    "check_accuracy_part34(loader_test, best_model)"
   ], 
   "outputs": [], 
   "metadata": {}
  }
 ], 
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3", 
   "name": "python3", 
   "language": "python"
  }, 
  "toc": {
   "nav_menu": {}, 
   "toc_window_display": false, 
   "toc_section_display": "block", 
   "sideBar": true, 
   "number_sections": true, 
   "skip_h1_title": false, 
   "toc_position": {}, 
   "toc_cell": false
  }, 
  "varInspector": {
   "kernels_config": {
    "python": {
     "delete_cmd_postfix": "", 
     "library": "var_list.py", 
     "delete_cmd_prefix": "del ", 
     "varRefreshCmd": "print(var_dic_list())"
    }, 
    "r": {
     "delete_cmd_postfix": ") ", 
     "library": "var_list.r", 
     "delete_cmd_prefix": "rm(", 
     "varRefreshCmd": "cat(var_dic_list()) "
    }
   }, 
   "cols": {
    "lenType": 16, 
    "lenName": 16, 
    "lenVar": 40
   }, 
   "types_to_exclude": [
    "module", 
    "function", 
    "builtin_function_or_method", 
    "instance", 
    "_Feature"
   ], 
   "window_display": false
  }, 
  "language_info": {
   "mimetype": "text/x-python", 
   "nbconvert_exporter": "python", 
   "name": "python", 
   "file_extension": ".py", 
   "version": "3.7.1", 
   "pygments_lexer": "ipython3", 
   "codemirror_mode": {
    "version": 3, 
    "name": "ipython"
   }
  }
 }
}
